package io.gitee.zhangbinhub.acp.cloud.resource.server

import com.fasterxml.jackson.databind.ObjectMapper
import io.gitee.zhangbinhub.acp.boot.AcpExceptionAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.AcpCloudComponentAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.AcpCloudPropertiesAutoConfiguration
import io.gitee.zhangbinhub.acp.cloud.constant.RestPrefix
import io.gitee.zhangbinhub.acp.cloud.resource.server.conf.AcpCloudResourceServerConfiguration
import io.gitee.zhangbinhub.acp.cloud.resource.server.error.*
import io.gitee.zhangbinhub.acp.core.client.exceptions.HttpException
import io.gitee.zhangbinhub.acp.core.client.http.HttpClientBuilder
import io.gitee.zhangbinhub.acp.core.common.CommonTools
import io.netty.channel.ChannelOption
import io.netty.handler.timeout.ReadTimeoutHandler
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration
import org.springframework.boot.autoconfigure.web.ServerProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties
import org.springframework.context.annotation.Bean
import org.springframework.http.MediaType
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.http.client.support.BasicAuthenticationInterceptor
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.http.codec.json.Jackson2JsonEncoder
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver
import org.springframework.web.client.RestTemplate
import org.springframework.web.reactive.function.client.WebClient
import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit

/**
 * @author zhangbin by 2018-3-14 15:13
 * @since JDK 11
 */
@AutoConfiguration(
    before = [OAuth2ResourceServerAutoConfiguration::class],
    after = [AcpCloudPropertiesAutoConfiguration::class, AcpCloudComponentAutoConfiguration::class, AcpExceptionAutoConfiguration::class]
)
@EnableConfigurationProperties(AcpCloudResourceServerConfiguration::class)
class AcpCloudResourceServerComponentAutoConfiguration(
    private val acpCloudResourceServerConfiguration: AcpCloudResourceServerConfiguration,
    serverProperties: ServerProperties
) {
    private val contextPath: String =
        if (CommonTools.isNullStr(serverProperties.servlet.contextPath)) "" else serverProperties.servlet.contextPath

    @Bean
    fun webSecurityErrorHandler() = WebSecurityErrorHandler()

    /**
     * 自定义负载均衡客户端
     *
     * @return RestTemplate
     * @throws HttpException 异常
     */
    @LoadBalanced
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @RegisterReflectionForBinding(classes = [RestTemplate::class])
    @Bean("acpSpringCloudResourceServerRestTemplate")
    @Throws(HttpException::class)
    fun acpSpringCloudResourceServerRestTemplate(
        feignHttpClientProperties: FeignHttpClientProperties, objectMapper: ObjectMapper
    ): RestTemplate = RestTemplate(
        OkHttp3ClientHttpRequestFactory(
            HttpClientBuilder().maxTotalConn(feignHttpClientProperties.maxConnections)
                .connectTimeOut(feignHttpClientProperties.connectionTimeout.toLong())
                .readTimeOut(feignHttpClientProperties.okHttp.readTimeout.toMillis())
                .timeToLive(feignHttpClientProperties.timeToLive)
                .timeToLiveTimeUnit(feignHttpClientProperties.timeToLiveUnit)
                .followRedirects(feignHttpClientProperties.isFollowRedirects)
                .disableSslValidation(feignHttpClientProperties.isDisableSslValidation).build().builder.build()
        )
    ).apply {
        this.messageConverters.add(MappingJackson2HttpMessageConverter(objectMapper))
    }.apply {
        this.interceptors.add(
            BasicAuthenticationInterceptor(
                acpCloudResourceServerConfiguration.clientId, acpCloudResourceServerConfiguration.clientSecret
            )
        )
    }

    @LoadBalanced
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
    @RegisterReflectionForBinding(classes = [WebClient.Builder::class])
    @Bean("acpSpringCloudResourceServerWebClientBuilder")
    fun acpSpringCloudResourceServerWebClientBuilder(
        feignHttpClientProperties: FeignHttpClientProperties, objectMapper: ObjectMapper
    ): WebClient.Builder = WebClient.builder().clientConnector(
        ReactorClientHttpConnector(HttpClient.create(
            ConnectionProvider.builder("acp-tcp-client-pool").maxConnections(feignHttpClientProperties.maxConnections)
                .maxIdleTime(
                    Duration.of(
                        feignHttpClientProperties.timeToLive, unitConvert(feignHttpClientProperties.timeToLiveUnit)
                    )
                ).build()
        ).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, feignHttpClientProperties.connectionTimeout).doOnConnected {
            it.addHandlerLast(
                ReadTimeoutHandler(
                    feignHttpClientProperties.okHttp.readTimeout.toMillis(), TimeUnit.MILLISECONDS
                )
            )
        })
    ).codecs { configuration ->
        configuration.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON))
        configuration.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
    }.defaultHeaders { headers ->
        headers.setBasicAuth(
            acpCloudResourceServerConfiguration.clientId, acpCloudResourceServerConfiguration.clientSecret
        )
    }

    private fun unitConvert(timeUnit: TimeUnit): ChronoUnit = when (timeUnit) {
        TimeUnit.NANOSECONDS -> ChronoUnit.NANOS
        TimeUnit.MICROSECONDS -> ChronoUnit.MICROS
        TimeUnit.MILLISECONDS -> ChronoUnit.MILLIS
        TimeUnit.SECONDS -> ChronoUnit.SECONDS
        TimeUnit.MINUTES -> ChronoUnit.MINUTES
        TimeUnit.HOURS -> ChronoUnit.HOURS
        TimeUnit.DAYS -> ChronoUnit.DAYS
        else -> ChronoUnit.MILLIS
    }

    @Bean
    @ConditionalOnMissingBean(BearerTokenResolver::class)
    fun bearerTokenResolver() = DefaultBearerTokenResolver().apply {
        this.setAllowFormEncodedBodyParameter(true)
        this.setAllowUriQueryParameter(true)
    }

    /**
     * 放行安全检查的开放Path
     */
    fun webPermitAllPath() = mutableListOf<String>().apply {
        this.add("$contextPath/error")
        this.add("$contextPath/favicon.ico")
        this.add("$contextPath/v3/api-docs/**")
        this.add("$contextPath/*.html")
        this.add("$contextPath/webjars/**")
        acpCloudResourceServerConfiguration.permitAllPath.forEach { path -> this.add(contextPath + path) }
        this.add(contextPath + RestPrefix.Open + "/**")
    }
}
